﻿// Copyright (c) 2009 http://grommet.codeplex.com
// This source is subject to the Microsoft Public License.
// See http://www.opensource.org/licenses/ms-pl.html
// All other rights reserved.

using System;
using System.ComponentModel;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Threading;
using Grommet.Ext;

#if MicroFramework
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
#endif

namespace Grommet.Net.XBee
{
    public class XBeeModule : IDisposable
    {
        private const int apiIdentifier = 0;
        private const int frameIdentifier = 1;

        private object syncObject = new object();
        private byte nextFrameID;

        private SerialDataReceivedEventHandler dataReceivedEventHandler;
        private SerialErrorReceivedEventHandler errorReceivedEventHandler;

        private ApiFrameBuilder frameBuilder;
        private ApiFrame[] frameBuffer; 

        public SerialPort SerialPort { get; set; }

        public CurrentNodeInfo CurrentNode { get; private set; }

        public event ModemStatusChangedEventHandler ModemStatusChanged;
        public event FrameReceivedEventHandler FrameReceived;
        public event IODataSampleReceivedHandler DataSampleReceived;

        /// <summary>
        /// Tests the specified COM port for an XBee device.
        /// </summary>
        /// <param name="portName"></param>
        /// <param name="baud"></param>
        /// <param name="apiMode"></param>
        /// <returns></returns>
        public static bool TestComPortForXbee( string portName, int baud, ApiMode apiMode )
        {
            bool isXbee = false;

            SerialPort sp = new SerialPort( portName );
            if ( !sp.IsOpen && sp.BaudRate == baud )
            {
                XBeeModule xbee = new XBeeModule( sp, apiMode );
                try
                {
                    // if this throws, its not an xbee
                    if ( xbee.TestOpen() )
                        isXbee = true;
                }
                catch ( Exception )
                {
                    isXbee = false;
                }

                if ( xbee.SerialPort.IsOpen )
                    xbee.SerialPort.Close();
            }

            return isXbee;
        }

        #region Class Lifetime

        public XBeeModule(string portName, int baudRate)
            : this(new SerialPort(portName, baudRate))
        {
        }

        public XBeeModule(SerialPort serialPort)
            : this(serialPort, ApiMode.ApiOperation)
        {
        }

        public XBeeModule(SerialPort serialPort, ApiMode apiMode)
        {
            this.SerialPort = serialPort;

            frameBuilder = new ApiFrameBuilder(apiMode);
            dataReceivedEventHandler = new SerialDataReceivedEventHandler(Serial_DataReceived);
            errorReceivedEventHandler = new SerialErrorReceivedEventHandler(Serial_ErrorReceived);
            frameBuffer = new ApiFrame[256];
            
            nextFrameID = 1;
            CurrentNode = new CurrentNodeInfo();
            CurrentNode.MaximumRFPayloadSize = 72;
        }

        ~XBeeModule()
        {
            Dispose(false);
        }

        #endregion

        #region Network Methods

        public void Open()
        {
            SerialPort.Open();
            SerialPort.DataReceived += dataReceivedEventHandler;
            SerialPort.ErrorReceived += errorReceivedEventHandler;

            AutoResetEvent ev = new AutoResetEvent(false);
            PropertyChangedEventHandler handler = (sender, e) => { if (e.PropertyName=="Identifier") ev.Set(); };
            CurrentNode.PropertyChanged += handler;
            CurrentNode.Refresh(this);
            ev.WaitOne();
            CurrentNode.PropertyChanged -= handler;
        }

        /// <summary>
        /// Opens the serial port and tests to see if an XBee device is attatched.
        /// </summary>
        /// <returns></returns>
        public bool TestOpen()
        {
            bool success = true;

            SerialPort.Open();
            SerialPort.DataReceived += dataReceivedEventHandler;
            SerialPort.ErrorReceived += errorReceivedEventHandler;

            AutoResetEvent ev = new AutoResetEvent(false);

            SendCommand(XBeeCommand.SerialNumberLow,
                delegate(ApiFrame frame)
                {
                    ev.Set();
                }
            );

            if (!ev.WaitOne(500))
                success = false;

            SerialPort.DataReceived -= dataReceivedEventHandler;
            SerialPort.ErrorReceived -= errorReceivedEventHandler;

            return success;
        }

        public void Close()
        {
            SerialPort.Close();
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void RefreshCurrentNode()
        {
            CurrentNode.Refresh(this);
        }

        #endregion

        #region Low-level Frame Methods

        internal byte SendApiFrame(ApiFrame frame)
        {
            byte frameID = frame.FrameData[frameIdentifier];
            frameBuffer[frameID] = frame;

            try
            {
                SerialPort.Write(frame.Serialize(), 0, frame.Length + 4);
            }
            catch (Exception ex)
            {
                // If the port is closed, perhaps the user unplugged the device but later plugged it back in
                // try to reconnect...
                if (!RetrySendApiFrame(frame))
                    throw ex;
            }

            return frameID;
        }

        private bool RetrySendApiFrame(ApiFrame frame)
        {
            try
            {
                if (!SerialPort.IsOpen)
                    SerialPort.Open();
                SerialPort.Write(frame.Serialize(), 0, frame.Length + 4);
                return true;
            }
            catch ( Exception )
            {
                // Ignoring this exception and will re-throw the original one from the caller
                return false;
            }
        }

        private void ReceivedApiFrame(ApiFrame frame)
        {
            switch(frame.FrameData[apiIdentifier])
            {
                case (byte)ApiFrameName.ModemStatus:
                    OnModemStatusChanged((ModemStatus)frame.FrameData[1]);
                    break;

                case (byte)ApiFrameName.ATCommandResponse:
                    ReceivedATCommandResponse(frame);
                    break;

                case (byte)ApiFrameName.ZigBeeReceivePacket:
                    ReceivedZigBeePacket(frame);
                    break;

                case (byte)ApiFrameName.ZigBeeTransmitStatus:
                    ReceivedTransmitStatus(frame);
                    break;

                case (byte)ApiFrameName.RemoteCommandResponse:
                    ReceivedRemoteCommandResponse(frame);
                    break;

                case (byte)ApiFrameName.ZigBeeIODataSampleRxIndicator:
                    ReceivedZigBeeIODataSample(frame);
                    break;

                default:
                    break;
            }
        }

        internal void ReceivedATCommandResponse(ApiFrame frame)
        {
            byte frameID = frame.FrameData[frameIdentifier];
            XBeeCommand command = (XBeeCommand)BigEndianConvert.ToUInt16(frame.FrameData, 2);
            ApiFrame sentFrame = frameBuffer[frameID];
            
            if (sentFrame != null && sentFrame.Callback != null)
            {
                sentFrame.Callback(frame);
                if (!(command == XBeeCommand.NodeDiscover && frame.Length > 0))
                {
                    frameBuffer[frameID] = null;
                }
            }
        }

        internal void ReceivedRemoteCommandResponse(ApiFrame frame)
        {
            byte frameID = frame.FrameData[frameIdentifier];
            ApiFrame sentFrame = frameBuffer[frameID];

            if (sentFrame != null && sentFrame.Callback != null)
            {
                sentFrame.Callback(new RemoteCommandResponse(frame));
            }
            frameBuffer[frameID] = null;
        }

        internal void ReceivedZigBeePacket(ApiFrame frame)
        {
            ulong sourceSerialNumber = BigEndianConvert.ToUInt64(frame.FrameData, 1);
            ushort sourceAddress = BigEndianConvert.ToUInt16(frame.FrameData, 9);
            PacketOptions options = (PacketOptions)frame.FrameData[11];
            byte[] data = Utility.ExtractRangeFromArray(frame.FrameData, 12, frame.Length - 12);
            
            OnFrameReceived(sourceSerialNumber, sourceAddress, options, data);
        }

        internal void ReceivedTransmitStatus(ApiFrame frame)
        {
            byte frameID = frame.FrameData[frameIdentifier];
            ApiFrame sentFrame = frameBuffer[frameID];

            if (sentFrame != null && sentFrame.Callback != null )
            {
                sentFrame.Callback(new ZigBeeTransmitStatus(frame));
            }
            frameBuffer[frameID] = null;
        }

        /// <summary>
        /// Called when a remote device sends an IO data sample.
        /// </summary>
        /// <param name="frame"></param>
        internal void ReceivedZigBeeIODataSample(ApiFrame frame)
        {
            // Get source info
            ulong sourceSerialNumber = BigEndianConvert.ToUInt64(frame.FrameData, 1);
            ushort sourceAddress = BigEndianConvert.ToUInt16(frame.FrameData, 9);

            // What IO did it send us?
            ushort digitalChannelMask = BigEndianConvert.ToUInt16(frame.FrameData, 13);
            byte analogChannelMask = frame.FrameData[15];
            ushort digitalSamples = 0;
            ushort[] analogSamples = new ushort[8] {0, 0, 0, 0, 0, 0, 0, 0};

            int index = 16;

            // Did we get digital data?
            if (digitalChannelMask > 0)
            {
                digitalSamples = BigEndianConvert.ToUInt16(frame.FrameData, index);
                index += 2;
            }

            // Did we get analog data?
            if (analogChannelMask > 0)
            {
                byte pos = 0;

                // walk through the analog channel mask and fill the array with data if the cooresponding bit in the mask is set
                while (pos < 8)
                {
                    byte mask = (byte)(analogChannelMask >> pos);
                    if ((mask & 0x01) == 1)
                    {
                        analogSamples[pos] = BigEndianConvert.ToUInt16(frame.FrameData, index);
                        index += 2;
                    }
                    pos++;
                }
            }

            OnDataSampleReceived(sourceSerialNumber, sourceAddress, digitalChannelMask, digitalSamples, analogChannelMask, analogSamples);
        }

        #endregion

        #region High-level Frame Methods

        #region Send AT Command

        public byte SendCommand(XBeeCommand command, XBeeCallback callback)
        {
            return SendCommand(command, callback, 0, 0);
        }

        public byte SendCommand(XBeeCommand command, XBeeCallback callback, byte value)
        {
            return SendCommand(command, callback, value, 1);
        }

        public byte SendCommand(XBeeCommand command, XBeeCallback callback, ushort value)
        {
            return SendCommand(command, callback, value, 2);
        }

        public byte SendCommand(XBeeCommand command, XBeeCallback callback, uint value)
        {
            return SendCommand(command, callback, value, 4);
        }

        public byte SendCommand(XBeeCommand command, XBeeCallback callback, string value)
        {
            ApiFrame frame = new ApiFrame();
            byte frameID = GetNextFrameID();
            byte[] buffer = new byte[4];

            buffer[apiIdentifier] = (byte)ApiFrameName.ATCommand;
            buffer[frameIdentifier] = frameID;
            Utility.InsertValueIntoArray(buffer, 2, 2, (uint)command);
            byte[] valueBytes = Utility.CombineArrays(Encoding.UTF8.GetBytes(value), new byte[] { 0 });
            frame.FrameData = Utility.CombineArrays(buffer, valueBytes);
            frame.Callback = callback;

            return SendApiFrame(frame);
        }

        private byte SendCommand(XBeeCommand command, XBeeCallback callback, uint value, int size)
        {
            ApiFrame frame = new ApiFrame();
            byte frameID = GetNextFrameID();
            byte[] buffer = new byte[4 + size];

            buffer[apiIdentifier] = (byte)ApiFrameName.ATCommand;
            buffer[frameIdentifier] = frameID;
            byte[] cmdBuffer = BigEndianConvert.GetBytes((ushort)command);
            buffer[2] = cmdBuffer[0];
            buffer[3] = cmdBuffer[1];
            if (size > 0)
            {
                Utility.InsertValueIntoArray(buffer, 4, size, value);
            }
            frame.FrameData = buffer;
            frame.Callback = callback;

            return SendApiFrame(frame);
        }

        #endregion

        #region Send RF Payload

        public byte SendString(ulong destinationSerialNumber, ushort destinationAddress, string message, XBeeCallback callback)
        {
            return SendData(destinationSerialNumber, destinationAddress, Encoding.UTF8.GetBytes(message), callback);
        }

        public byte SendData(ulong destinationSerialNumber, ushort destinationAddress, byte[] data, XBeeCallback callback)
        {
            ApiFrame frame = new ApiFrame();
            byte[] buffer = new byte[2 + sizeof(ulong) + sizeof(ushort) + 2];
            buffer[apiIdentifier] = (byte)ApiFrameName.ZigBeeTransmitRequest;
            buffer[frameIdentifier] = GetNextFrameID();

            if (destinationAddress == 0)
            {
                // only the coordinator has a destination address of 0, and to get this frame to
                // the coordinator, we need to set the serial number to 0 as well no matter what
                // was passed in.
                destinationSerialNumber = 0;
            }

            Utility.InsertValueIntoArray(buffer, 2, sizeof(uint), (uint)(destinationSerialNumber >> 32));
            Utility.InsertValueIntoArray(buffer, 6, sizeof(uint), (uint)(destinationSerialNumber & 0xFFFFFFFF));
            Utility.InsertValueIntoArray(buffer, 10, sizeof(ushort), destinationAddress);
            Utility.InsertValueIntoArray(buffer, 12, sizeof(ushort), 0);
            frame.FrameData = Utility.CombineArrays(buffer, data);
            frame.Callback = callback;

            return SendApiFrame(frame);
        }

        public byte SendRemoteCommand(ulong destinationSerialNumber, ushort destinationAddress, XBeeCommand command, byte value, XBeeCallback callback)
        {
            return SendRemoteCommand(destinationSerialNumber, destinationAddress, command, value, 1, callback);
        }

        public byte SendRemoteCommand(ulong destinationSerialNumber, ushort destinationAddress, XBeeCommand command, ushort value, XBeeCallback callback)
        {
            return SendRemoteCommand(destinationSerialNumber, destinationAddress, command, value, 2, callback);
        }

        public byte SendRemoteCommand(ulong destinationSerialNumber, ushort destinationAddress, XBeeCommand command, uint value, XBeeCallback callback)
        {
            return SendRemoteCommand(destinationSerialNumber, destinationAddress, command, value, 4, callback);
        }

        private byte SendRemoteCommand(ulong destinationSerialNumber, ushort destinationAddress, XBeeCommand command, uint value, int size, XBeeCallback callback)
        {
            ApiFrame frame = new ApiFrame();
            byte frameID = GetNextFrameID();
            byte[] buffer = new byte[2 + sizeof(ulong) + sizeof(ushort) + 3 + size];
            buffer[apiIdentifier] = ( byte )ApiFrameName.RemoteCommandRequest;
            buffer[frameIdentifier] = frameID;

            // frame bytes 6-13: 64-bit destination address
            if (destinationAddress == 0)
            {
                // only the coordinator has a destination address of 0, and to get this frame to
                // the coordinator, we need to set the serial number to 0 as well no matter what
                // was passed in.
                destinationSerialNumber = 0;
            }

            Utility.InsertValueIntoArray(buffer, 2, sizeof(uint), (uint)(destinationSerialNumber >> 32));
            Utility.InsertValueIntoArray(buffer, 6, sizeof(uint), (uint)(destinationSerialNumber & 0xFFFFFFFF));

            // frame bytes 14-15: destination network address
            Utility.InsertValueIntoArray(buffer, 10, sizeof(ushort), destinationAddress);

            // frame byte 16: command options (0x2 = apply changes on remote)
            buffer[12] = 0x02;

            // frame bytes 17-18: command name (ASCII command name, such as D0)
            Utility.InsertValueIntoArray(buffer, 13, sizeof(ushort), (ushort)command);

            // frame bytes 19-n: command data
            Utility.InsertValueIntoArray(buffer, 15, size, value);

            frame.FrameData = buffer;
            frame.Callback = callback;

            return SendApiFrame(frame);
        }

        #endregion

        #endregion

        #region Serial Port Event Handlers

        private void Serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (SerialPort.BytesToRead <= 0)
            {
                // not sure why we are here, but there is nothing to do
                return;
            }

            byte[] buf = new byte[1];

            while (SerialPort.BytesToRead > 0)
            {
                SerialPort.Read(buf, 0, 1);
                frameBuilder.Append(buf[0]);
                if (frameBuilder.IsComplete)
                {
                    // got a frame, do something
                    ReceivedApiFrame(frameBuilder.GetApiFrame());
                    // Create a thread, launch fire event, etc
                    frameBuilder.Reset();
                }
            }
        }

        private void Serial_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
            frameBuilder.Reset();
        }

        #endregion

        #region Bookkeeping

        public void DropCallback(byte frameID)
        {
            if (frameBuffer[frameID] != null)
            {
                frameBuffer[frameID].Callback = null;
                frameBuffer[frameID] = null;
            }
        }


        private byte GetNextFrameID()
        {
            lock (syncObject)
            {
                nextFrameID++;
                if (nextFrameID > 255)
                {
                    nextFrameID = 2;
                }
                return nextFrameID;
            }
        }

        #endregion

        #region Fire Methods

        private void OnModemStatusChanged(ModemStatus status)
        {
            ModemStatusChangedEventHandler handler = ModemStatusChanged;
            if (handler != null)
            {
                handler(this, new ModemStatusChangedEventArgs(status));
            }
        }

        private void OnFrameReceived(ulong sourceSerialNumber, ushort sourceAddress, PacketOptions options, byte[] data)
        {
            FrameReceivedEventHandler handler = FrameReceived;
            if (handler != null)
            {
                handler(this, new FrameReceivedEventArgs(sourceSerialNumber, sourceAddress, options, data));
            }
        }

        private void OnDataSampleReceived( ulong sourceSerialNumber, ushort sourceAddress, ushort digitalMask, ushort digitalSamples, byte analogMask, ushort[] analogSamples )
        {
            IODataSampleReceivedHandler handler = DataSampleReceived;
            if (handler != null)
            {
                handler(this, new IODataSampleReceivedArgs(sourceSerialNumber, sourceAddress, digitalMask, digitalSamples, analogMask, analogSamples));
            }
        }

        #endregion

        #region IDisposable Members

        private bool disposed = false;  // detects redundent calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    if (SerialPort != null)
                    {
                        SerialPort.DataReceived -= dataReceivedEventHandler;
                        SerialPort.ErrorReceived -= errorReceivedEventHandler;
                    }
                }
                disposed = true;
            }
        }

        public void Dispose()
        {
            Close();
        }

        #endregion
    }
}
